Skip to content

Laurel: Add constrained type support#505

Merged
fabiomadge merged 11 commits intomainfrom
fabiomadge/constrained-types
Mar 12, 2026
Merged

Laurel: Add constrained type support#505
fabiomadge merged 11 commits intomainfrom
fabiomadge/constrained-types

Conversation

@fabiomadge
Copy link
Contributor

@fabiomadge fabiomadge commented Mar 3, 2026

Constrained Types for Laurel

Adds constrained types to Laurel via a Laurel-to-Laurel elimination pass that inserts verification checks at type boundaries.

Syntax

constrained nat = x: int where x >= 0 witness 0
constrained posnat = x: nat where x != 0 witness 1

How it works

The pass (ConstrainedTypeElim.lean) generates a named constraint function per constrained type (e.g. function nat$constraint(x: int): bool { x >= 0 }) and eliminates constrained types by:

  1. requires constraintFunc(param) for constrained-typed inputs — Core handles caller asserts and body assumes via call elimination
  2. ensures constraintFunc(result) for constrained-typed outputs — Core handles body checks and caller assumes
  3. assert constraintFunc(var) after local variable init and reassignment of constrained-typed variables
  4. Witness as default for uninitialized constrained-typed variables
  5. Witness validation via synthetic procedures that assert the witness satisfies the constraint function
  6. Quantifier constraint injectionforall(n: nat) => body becomes forall(n: int) => nat$constraint(n) ==> body; exists uses &&
  7. Type resolution — all constrained type references resolved to base types

The Core translator sees only base types and regular requires/ensures/assert with constraint function calls — no translator changes needed beyond pipeline wiring.

Functions

Functions (isFunctional procedures) with constrained return types emit a "constrained return types on functions are not yet supported" diagnostic. The function is translated as-is with types resolved, but the return constraint is not checked.

Changes

  • ConstrainedTypeElim.lean — the elimination pass
  • ConstrainedTypeElimTest.lean — before/after comparison test and scope regression test
  • LaurelGrammar.st / LaurelGrammar.lean — constrained type syntax
  • ConcreteToAbstractTreeTranslator.lean — parser for constrained keyword
  • LaurelToCoreTranslator.lean — pipeline wiring (import + pass + resolve + diagnostics)
  • T10_ConstrainedTypes.lean — 24 test procedures covering inputs, outputs, assignments, arguments, nested types, functions, witnesses, quantifiers, capture avoidance

Known limitations

  • resolveBaseType is partial — cyclic constrained type definitions loop forever

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch from 1c60132 to b2d41d6 Compare March 3, 2026 07:41
@fabiomadge fabiomadge force-pushed the fabiomadge/early-return-soundness branch 3 times, most recently from 579e349 to dd1139d Compare March 4, 2026 05:41
Base automatically changed from fabiomadge/early-return-soundness to main March 5, 2026 15:37
@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch 8 times, most recently from fa3f500 to 566122e Compare March 6, 2026 04:48
@fabiomadge fabiomadge changed the title Laurel: add constrained type support Laurel: Add constrained type support Mar 6, 2026
@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch from 566122e to e2ff8b6 Compare March 9, 2026 04:24
@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch 5 times, most recently from 9c8f422 to afd9ee8 Compare March 9, 2026 05:29
@fabiomadge fabiomadge marked this pull request as ready for review March 9, 2026 05:40
@fabiomadge fabiomadge requested a review from a team March 9, 2026 05:40
Copy link
Contributor

@keyboardDrummer keyboardDrummer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code quality looks great but I have some feedback on the design.

Also, could you add a test that compares the Laurel before and after the phase? Similar to StrataTest/Languages/Laurel/LiftExpressionAssignmentsTest.lean

@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch 2 times, most recently from 56da20a to d4f2563 Compare March 10, 2026 01:30
@fabiomadge fabiomadge marked this pull request as draft March 12, 2026 01:17
@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch from 056055c to f210ea6 Compare March 12, 2026 01:27
github-merge-queue bot pushed a commit that referenced this pull request Mar 12, 2026
Change the `LocalVariable none` case to emit Core `init` without RHS
(havoc) instead of `defaultExprForType`. Source language translators
that need default values should add explicit initializers at the Laurel
level.

This is the correct semantics for Laurel as a verification IR: an
uninitialized variable has no known value until assigned. The previous
behavior (defaulting to 0/false/"") was introduced in #435 for
Python→Laurel but doesn't generalize to other source languages (e.g.,
JS/TS where uninitialized means `undefined`, not 0).

Enables a follow-up in #505 to switch constrained type uninitialized
variables from witness injection + assert to assume.

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

Co-authored-by: Shilpi Goel <shigoel@gmail.com>
@fabiomadge fabiomadge marked this pull request as ready for review March 12, 2026 02:45
@fabiomadge fabiomadge enabled auto-merge March 12, 2026 02:46
A Laurel-to-Laurel elimination pass (ConstrainedTypeElim.lean) that:
- Adds requires for constrained-typed inputs
- Adds ensures for constrained-typed outputs
- Clears isFunctional when adding ensures (function postconditions not yet supported)
- Inserts assert for local variable init and reassignment
- Uses witness as default initializer for uninitialized constrained variables
- Validates witnesses via synthetic procedures
- Injects constraints into quantifier bodies (forall → implies, exists → and)
- Resolves all constrained type references to base types
- Handles capture avoidance in identifier substitution

Core's call elimination handles caller-side argument asserts and
return value assumes automatically via requires/ensures.

Grammar: constrained type syntax
Parser: parseConstrainedType + topLevelConstrainedType
Test: T09_ConstrainedTypes — 25 test procedures
Save/restore PredVarMap state around Block, IfThenElse, and While via
inScope helper to prevent constrained variable entries from leaking
across sibling scopes.

Reported by shigoel: a constrained variable declared in an if-branch
would cause spurious asserts on same-named variables in sibling blocks.
Generate named constraint functions (e.g. nat$constraint) instead of
inlining substituted constraint expressions. This eliminates substId
(with its partial annotation and capture avoidance complexity) and
produces clearer Core output with named function calls.

Constraint functions are placed before user procedures to ensure
resolution processes them first (workaround for resolution ID
assignment order dependency).
Generate named constraint functions (e.g. nat$constraint) instead of
inlining substituted constraint expressions. This eliminates substId
(with its partial annotation and capture avoidance complexity) and
produces clearer Core output with named function calls.

Constraint functions are placed before user procedures so that
resolution assigns their IDs before resolving references in user
procedure bodies.

Also changes posnat constraint to x != 0 per review suggestion.
…rained variables

Include translator fix: emit init without RHS (havoc) for uninitialized
variables instead of defaultExprForType.

For uninitialized constrained variables, emit assume instead of assert:
  var x: posint;  →  var x: int; assume posint$constraint(x);

The witness is now only used in witness validation procedures.
@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch from 0f8fc58 to 7526c9f Compare March 12, 2026 02:53
MikaelMayer
MikaelMayer previously approved these changes Mar 12, 2026
@fabiomadge fabiomadge force-pushed the fabiomadge/constrained-types branch from ac8acc2 to f75d8b8 Compare March 12, 2026 13:40
@fabiomadge fabiomadge added this pull request to the merge queue Mar 12, 2026
Merged via the queue into main with commit cf1e598 Mar 12, 2026
15 checks passed
@fabiomadge fabiomadge deleted the fabiomadge/constrained-types branch March 12, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants